import * as XLSX from 'xlsx';

import {AbstractInvokeExpr, ArkAssignStmt,
    ArkClass, ArkFile, ArkInvokeStmt, ArkMethod, FileSignature, fileSignatureCompare, MethodSignature, NullType, Scene, SceneConfig,
    Stmt, UndefinedType, UnionType,
    Value} from 'arkanalyzer';
import { MethodParameter } from 'arkanalyzer/lib/core/model/builder/ArkMethodBuilder';

export interface Sdk {
    name: string;
    path: string;
    moduleName: string;
}

let argumentsWithExtraNull: Stmt[] = [];
let argumentsWithExtraUndefined: Stmt[] = [];

function buildScene(): Scene | null {
    try {
        const filesPath: string[] = ['./'];
        const projectDir = 'D:\\tests\\TestApplication';
        const projectName = 'TestApplication';
        const sdks: Sdk[] = [
            {
                name: 'hms',
                path: 'D:\\sdk\\default\\hms\\ets',
                moduleName: ''
            },
            {
                name: 'openharmony',
                path: 'D:\\sdk\\default\\openharmony\\ets',
                moduleName: ''
            }
        ];
        const sceneConfig: SceneConfig = new SceneConfig();
        sceneConfig.buildFromProjectFiles(projectName, projectDir, filesPath, sdks);

        const scene: Scene = new Scene();
        scene.buildSceneFromFiles(sceneConfig);
        scene.inferTypes();
        return scene;
    } catch (error) {
        console.error('Build scene or infer types error: ', error);
        return null;
    }
}

function checkSdkParam(): void {
    const scene = buildScene();
    if (scene === null) {
        return;
    }
    const arkFiles = scene.getFiles();
    const sdkFiles = scene.getSdkArkFiles();
    console.log(`successfully get scene with files: ${arkFiles.length}`);
    console.log(`successfully get scene with files: ${sdkFiles.length}`);

    for (const arkFile of arkFiles) {
        for (const arkClass of arkFile.getClasses()) {
            checkArkClass(arkClass, scene);
        }
        for (const namespace of arkFile.getNamespaces()) {
            for (const arkClass of namespace.getClasses()) {
                checkArkClass(arkClass, scene);
            }
        }
    }
}

function checkArkClass(arkClass: ArkClass, scene: Scene): void {
    for (const arkMethod of arkClass.getMethods()) {
        const stmts = arkMethod.getCfg()?.getStmts();
        if (stmts === undefined) {
            continue;
        }
        for (const stmt of stmts) {
            if (stmt instanceof ArkInvokeStmt) {
                checkInvokeStmt(stmt, scene);
            } else if (stmt instanceof ArkAssignStmt && stmt.getRightOp() instanceof AbstractInvokeExpr) {
                checkInvokeStmt(stmt, scene);
            }
        }
    }
}

function checkInvokeStmt(stmt: ArkInvokeStmt | ArkAssignStmt, scene: Scene): void {
    let invokeExpr: AbstractInvokeExpr;
    if (stmt instanceof ArkInvokeStmt) {
        invokeExpr = stmt.getInvokeExpr();
    } else {
        const rightOp = stmt.getRightOp();
        if (rightOp instanceof AbstractInvokeExpr) {
            invokeExpr = rightOp;
        } else {
            return;
        }
    }
    const callMethodSignature = invokeExpr.getMethodSignature();
    const projectName = callMethodSignature.getDeclaringClassSignature().getDeclaringFileSignature().getProjectName();
    // 如果是sdk方法的调用，则方法签名中的projectName应该是sdkMap的key之一，否则不是sdk的方法调用
    if (!scene.getProjectSdkMap().has(projectName)) {
        return;
    }
    const parameters = callMethodSignature.getMethodSubSignature().getParameters();
    const args = invokeExpr.getArgs();
    // 对于重载场景，如果签名无法严格匹配的话，ArkAnalyzer的类型推导是返回找到的第一个签名
    if (parameters.length !== args.length) {
        return;
    }
    for (let index = 0; index < parameters.length; index++) {
        const paramType = parameters[index].getType();
        const argType = args[index].getType();
        if (paramType.toString() === argType.toString()) {
            continue;
        }
        if (argType instanceof UnionType) {
            if (paramType instanceof UnionType) {
                if (isNullInUnionType(argType) && !isNullInUnionType(paramType)) {
                    argumentsWithExtraNull.push(stmt);
                }
                if (isUndefinedInUnionType(argType) && !isUndefinedInUnionType(paramType)) {
                    argumentsWithExtraUndefined.push(stmt);
                }
            } else {
                if (isNullInUnionType(argType)) {
                    argumentsWithExtraNull.push(stmt);
                }
                if (isUndefinedInUnionType(argType)) {
                    argumentsWithExtraUndefined.push(stmt);
                }
            }
            return;
        }
    }
}

function isNullInUnionType(unionType: UnionType): boolean {
    for (const item of unionType.getTypes()) {
        if (item instanceof NullType) {
            return true;
        }
    }
    return false;
}

function isUndefinedInUnionType(unionType: UnionType): boolean {
    for (const item of unionType.getTypes()) {
        if (item instanceof UndefinedType) {
            return true;
        }
    }
    return false;
}

function findSdkMethodWithSignature(methodSignature: MethodSignature, sdkFiles: ArkFile[]): ArkMethod | null {
    const classSignature = methodSignature.getDeclaringClassSignature();
    const fileSignature = classSignature.getDeclaringFileSignature();
    const sdkFile = findSdkFileWithSignature(fileSignature, sdkFiles);
    if (sdkFile === null) {
        return null;
    }
    const sdkClass = sdkFile.getClass(classSignature);
    if (sdkClass === null) {
        return null;
    }
    return sdkClass.getMethod(methodSignature);
}

function findSdkFileWithSignature(fileSignature: FileSignature, sdkFiles: ArkFile[]): ArkFile | null {
    for (const file of sdkFiles) {
        if (fileSignatureCompare(fileSignature, file.getFileSignature())) {
            return file;
        }
    }
    return null;
}

function getValueArrayTypeString(values: Value[]): string {
    const typeStr: string[] = [];
    values.forEach(value => {
        typeStr.push(value.getType().toString());
    });
    return typeStr.join(', ');
}

function getParameterArrayTypeString(params: MethodParameter[]): string {
    const typeStr: string[] = [];
    params.forEach(param => {
        typeStr.push(param.getType().toString());
    });
    return typeStr.join(', ');
}

function generateDataFromStmt(stmt: ArkInvokeStmt | ArkAssignStmt): string[] | null {
    let invokeExpr: AbstractInvokeExpr;
    if (stmt instanceof ArkInvokeStmt) {
        invokeExpr = stmt.getInvokeExpr();
    } else {
        if (stmt.getRightOp() instanceof AbstractInvokeExpr) {
            invokeExpr = stmt.getRightOp() as AbstractInvokeExpr
        } else {
            return null;
        }
    }
    const methodName = invokeExpr.getMethodSignature().getMethodSubSignature().getMethodName();
    const sdkClass = invokeExpr.getMethodSignature().getDeclaringClassSignature().getClassName();
    const sdkFile = invokeExpr.getMethodSignature().getDeclaringClassSignature().getDeclaringFileSignature().toString();
    const callMethod = stmt.getCfg().getDeclaringMethod().getName();
    const callClass = stmt.getCfg().getDeclaringMethod().getDeclaringArkClass().getName();
    const callFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkClass().getDeclaringArkFile().getFileSignature().getFileName();
    const callArguments = getValueArrayTypeString(invokeExpr.getArgs());
    const SDKParameters = getParameterArrayTypeString(invokeExpr.getMethodSignature().getMethodSubSignature().getParameters());
    return [sdkFile, sdkClass, methodName, callFile, callClass, callMethod, callArguments, SDKParameters];
}

function writeData2Excel(argumentsWithExtraUndefined: Stmt[], argumentsWithExtraNull: Stmt[]): void {
    let data: string[][] = [['SDKFile', 'SDKClassName', 'SDKMethodName', 'CalledByFile', 'CalledByClass', 'CalledByMethod', 'CallArguments', 'SDKParameters', 'WithExtraNull', 'WithExtraUndefined']];
    for (const stmt of argumentsWithExtraUndefined) {
        if (stmt instanceof ArkInvokeStmt || stmt instanceof ArkAssignStmt) {
            const dataItem = generateDataFromStmt(stmt);
            if (dataItem === null) {
                continue;
            }
            dataItem.push('No', 'Yes');
            data.push(dataItem);
        }
    }
    for (const stmt of argumentsWithExtraNull) {
        if (stmt instanceof ArkInvokeStmt || stmt instanceof ArkAssignStmt) {
            const dataItem = generateDataFromStmt(stmt);
            if (dataItem === null) {
                continue;
            }
            dataItem.push('Yes', 'No');
            data.push(dataItem);
        }
    }
    const workbook = XLSX.utils.book_new();
    const worksheet = XLSX.utils.aoa_to_sheet(data);
    XLSX.utils.book_append_sheet(workbook, worksheet, 'data');
    XLSX.writeFile(workbook, 'output.xlsx');
    console.log(`Data with Extra null: ${argumentsWithExtraNull.length}`);
    console.log(`Data with Extra undefined: ${argumentsWithExtraUndefined.length}`);
    console.log('Successfully generate the output file');
}

checkSdkParam();
writeData2Excel(argumentsWithExtraUndefined, argumentsWithExtraNull);